Skip to content

fix: 将上游空回复纳入回退重试并将空回复报错显示传递给用户#6454

Open
a61995987 wants to merge 1 commit intoAstrBotDevs:devfrom
a61995987:fix-将上游空回复纳入回退重试

Hidden character warning

The head ref may contain hidden characters: "fix-\u5c06\u4e0a\u6e38\u7a7a\u56de\u590d\u7eb3\u5165\u56de\u9000\u91cd\u8bd5"
Open

fix: 将上游空回复纳入回退重试并将空回复报错显示传递给用户#6454
a61995987 wants to merge 1 commit intoAstrBotDevs:devfrom
a61995987:fix-将上游空回复纳入回退重试

Conversation

@a61995987
Copy link
Copy Markdown
Contributor

@a61995987 a61995987 commented Mar 16, 2026

fix:将上游空回复纳入回退重试并将空回复报错显示传递给用户
PR(#5610)的分散提交

Modifications / 改动点

修改文件:astrbot\core\agent\runners\tool_loop_agent_runner.py

  1. 增加空回复判断辅助函数_is_empty_llm_response
    在_iter_llm_responses_with_fallback方法中的async for resp in self._iter_llm_responses(include_model=idx == 0)循环中使用_is_empty_llm_response来判断回复是否合法 并在不合法时触发回退机制

  2. LLM returned empty assistant message with no tool calls.警告增加自定义的错误提醒 避免静默 让用户能够及时了解到模型执行结果 避免无意义的等待

修改文件:astrbot\core\exceptions.py

  1. 增加了一个LLMEmptyResponseError自定义错误 以表示空回复

Screenshots or Test Results / 运行截图或测试结果


Checklist / 检查清单

  • 😊 If there are new features added in the PR, I have discussed it with the authors through issues/emails, etc.
    / 如果 PR 中有新加入的功能,已经通过 Issue / 邮件等方式和作者讨论过。

  • 👀 My changes have been well-tested, and "Verification Steps" and "Screenshots" have been provided above.
    / 我的更改经过了良好的测试,并已在上方提供了“验证步骤”和“运行截图”

  • 🤓 I have ensured that no new dependencies are introduced, OR if new dependencies are introduced, they have been added to the appropriate locations in requirements.txt and pyproject.toml.
    / 我确保没有引入新依赖库,或者引入了新依赖库的同时将其添加到 requirements.txtpyproject.toml 文件相应位置。

  • 😮 My changes do not introduce malicious code.
    / 我的更改没有引入恶意代码。

Summary by Sourcery

Handle empty LLM responses by triggering provider fallback and surfacing explicit errors to users when all fallbacks are exhausted.

Bug Fixes:

  • Treat upstream empty LLM responses without tool calls as failures that trigger fallback to the next provider.
  • Raise a dedicated error when all fallback providers return empty assistant messages so users are informed instead of experiencing silent timeouts.

Enhancements:

  • Introduce a helper to detect effectively empty LLM responses based on text, reasoning content, tool calls, and result chain content.
  • Add a specific LLMEmptyResponseError exception to represent empty LLM replies with contextual metadata in the error message.

@auto-assign auto-assign bot requested review from Soulter and anka-afk March 16, 2026 15:28
@dosubot dosubot bot added the size:M This PR changes 30-99 lines, ignoring generated files. label Mar 16, 2026
@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

此拉取请求旨在改进系统对大型语言模型(LLM)空回复的处理。通过引入一个专门的辅助函数来识别空回复,并将其集成到现有的LLM回退机制中,确保当上游LLM提供者返回无意义的响应时,系统能够智能地尝试其他提供者。此外,当所有回退尝试都失败后,系统将抛出明确的错误,而不是静默失败,从而提高用户对模型执行结果的可见性和可操作性。

Highlights

  • 空回复判断辅助函数: 引入了_is_empty_llm_response辅助函数,用于精确判断LLM回复是否为空,包括文本、推理内容、工具调用和消息链。
  • LLM回退机制增强: 在LLM回退机制中集成了空回复判断,当LLM返回空回复且非最后一个候选时,会触发回退到下一个LLM提供者。
  • 自定义错误处理: 增加了LLMEmptyResponseError自定义异常,并在所有回退尝试失败后,当LLM仍返回空回复时抛出此异常,避免静默失败并向用户提供明确的错误信息。
Changelog
  • astrbot/core/agent/runners/tool_loop_agent_runner.py
    • 增加了_is_empty_llm_response方法,用于判断LLM响应是否为空。
    • 修改了_iter_llm_responses_with_fallback方法,使其在检测到空LLM响应时触发回退。
    • 更新了step方法,在所有回退失败后,如果LLM响应仍为空,则抛出LLMEmptyResponseError并提供详细上下文。
  • astrbot/core/exceptions.py
    • 新增了LLMEmptyResponseError异常类,用于表示LLM返回空回复的情况。
Activity
  • 此拉取请求由a61995987创建。
  • 目前没有其他评论、审查或进度更新。
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@dosubot dosubot bot added the area:provider The bug / feature is about AI Provider, Models, LLM Agent, LLM Agent Runner. label Mar 16, 2026
Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've left some high level feedback:

  • Consider making _is_empty_llm_response more reusable by returning early when resp.result_chain is None or has an empty chain, which will simplify the loop and avoid iterating when there's clearly no content.
  • Instead of only embedding model_id, provider_id, and run_id in the LLMEmptyResponseError message string, consider adding them as explicit attributes on the exception so callers can programmatically inspect the context.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Consider making `_is_empty_llm_response` more reusable by returning early when `resp.result_chain is None` or has an empty `chain`, which will simplify the loop and avoid iterating when there's clearly no content.
- Instead of only embedding `model_id`, `provider_id`, and `run_id` in the `LLMEmptyResponseError` message string, consider adding them as explicit attributes on the exception so callers can programmatically inspect the context.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@dosubot
Copy link
Copy Markdown

dosubot bot commented Mar 16, 2026

Related Documentation

1 document(s) may need updating based on files changed in this PR:

AstrBotTeam's Space

pr4697的改动
View Suggested Changes
@@ -1385,7 +1385,106 @@
 
 ---
 
-### 14. 其他优化
+### 14. 空响应检测与回退机制(PR #6454)
+
+#### 功能说明
+[PR #6454](https://github.com/AstrBotDevs/AstrBot/pull/6454) 增强了内置 Agent Runner 的空响应检测和回退处理,防止 LLM 返回空响应时导致的静默失败,确保用户能够及时获得清晰的错误提示。
+
+#### 问题背景
+修复前,当 LLM 返回空响应(无文本内容、无工具调用、无推理内容)时,系统仅记录警告日志 `"LLM returned empty assistant message with no tool calls."`,但不触发回退重试机制,也不向用户显示有意义的错误信息。这会导致:
+- 用户在等待响应时收到空白结果,无法理解发生了什么
+- 配置了多个回退 LLM 提供商时,系统不会尝试使用备用提供商
+- 空响应导致的问题难以排查,缺乏上下文信息
+
+#### 核心改进
+
+##### 1. 空响应检测辅助函数(`_is_empty_llm_response`)
+新增 `_is_empty_llm_response()` 辅助函数,用于检测 LLM 响应是否为空:
+
+**检测条件:**
+- `completion_text` 为空或仅包含空白字符
+- `reasoning_content` 为空或仅包含空白字符
+- `tools_call_args` 为空(无工具调用)
+- `result_chain` 无有效内容(不包含非空的 Plain 组件或其他类型组件,如图片、语音等)
+
+**实现细节:**
+- 对 `result_chain` 进行深度检查,跳过空的 Plain 组件
+- 非 Plain 组件(如 Image、Voice 等)视为有效内容
+- 返回 `True` 表示响应为空,`False` 表示响应有效
+
+##### 2. 回退重试机制集成
+在 `_iter_llm_responses_with_fallback()` 方法中集成空响应检测:
+
+**触发条件:**
+- LLM 响应角色为 `assistant` 或 `tool`
+- `_is_empty_llm_response()` 返回 `True`
+- 当前不是最后一个回退候选提供商
+
+**行为:**
+- 记录警告日志:`"Chat Model {candidate_id} returns empty response, trying fallback to next provider."`
+- 中断当前提供商的响应处理
+- 自动切换到下一个回退提供商
+- 流式响应的单个 chunk 不触发空响应检测(等待完整响应后再判断)
+
+##### 3. 新增异常类型(`LLMEmptyResponseError`)
+在 `astrbot/core/exceptions.py` 中新增 `LLMEmptyResponseError` 异常类型:
+
+**触发条件:**
+- 所有回退提供商均返回空响应
+- 系统无法通过回退机制获得有效响应
+
+**异常信息包含:**
+- `model_id`:当前使用的模型 ID
+- `provider_id`:当前使用的提供商 ID
+- `run_id`:当前运行的唯一标识符
+- 基础错误消息:`"LLM returned empty assistant message with no tool calls."`
+
+**异常格式示例:**
+```
+LLM returned empty assistant message with no tool calls. Context: model_id=gpt-4, provider_id=openai, run_id=abc123.
+```
+
+##### 4. 用户可见错误报告
+当所有回退提供商均失败后,系统会抛出 `LLMEmptyResponseError` 异常,用户将收到清晰的错误提示:
+
+**错误处理流程:**
+1. 系统尝试所有配置的回退提供商
+2. 如果所有提供商均返回空响应,抛出 `LLMEmptyResponseError`
+3. 错误消息包含上下文信息(model_id、provider_id、run_id),便于排障
+4. 用户收到明确的错误提示,而非静默失败或无意义的等待
+
+#### 技术实现要点
+
+##### 流式响应处理
+- 流式响应的单个 chunk 不立即触发空响应检测(`if resp.is_chunk: ...`)
+- 等待完整响应生成后(`resp.is_chunk=False`)再进行空响应检测
+- 避免因单个 chunk 的元数据或心跳消息触发误判
+
+##### 回退逻辑优化
+- 空响应检测仅在非最后一个回退候选提供商时触发回退
+- 确保系统尝试所有可用的回退选项
+- 最后一个提供商返回空响应时,抛出 `LLMEmptyResponseError` 而非继续重试
+
+##### 可观测性增强
+- 新增警告日志,记录触发回退的具体提供商
+- 异常消息包含完整的上下文信息,便于开发者和用户排障
+- 日志记录 LLM 响应为空的具体场景,帮助优化模型配置
+
+#### 用户收益
+- **避免静默失败**:空响应不再导致无声的失败,用户能够收到明确的错误提示
+- **自动回退**:配置多个提供商时,系统自动尝试备用提供商,提升可用性
+- **清晰错误信息**:包含上下文的错误消息帮助用户快速定位问题(模型配置、提供商状态等)
+- **提升可靠性**:防止空响应导致的无意义等待和用户体验下降
+
+#### 影响范围
+此修复适用于以下场景:
+- LLM 提供商返回空响应(无文本、无工具调用、无推理内容)
+- 配置了多个回退提供商的环境
+- 使用内置 Agent Runner(ToolLoopAgentRunner)的场景
+
+---
+
+### 15. 其他优化
 - JWT 处理和错误处理机制增强,提升系统安全性和稳定性
 - UI 细节优化,提升用户体验
 - 日志与异常处理增强,便于问题追踪

[Accept] [Decline]

Note: You must be authenticated to accept/decline updates.

How did I do? Any feedback?  Join Discord

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

本次 PR 的目标是处理上游 LLM 返回空回复的情况,通过增加回退重试机制和抛出明确的错误来改善用户体验。整体实现思路清晰,代码改动合理。

主要改动点包括:

  1. tool_loop_agent_runner.py 中新增了 _is_empty_llm_response 辅助函数来判断空回复,并在 _iter_llm_responses_with_fallback 中使用它来触发回退。
  2. 当所有回退尝试后仍然收到空回复时,会抛出 LLMEmptyResponseError 异常,并将包含上下文信息的错误传递给用户。
  3. exceptions.py 中定义了新的 LLMEmptyResponseError 异常。

代码质量较高,但有两处可以进行重构以提高代码的简洁性和可维护性,具体请见我的评论。

Comment on lines +240 to +251
has_result_chain_content = False
if resp.result_chain and resp.result_chain.chain:
for comp in resp.result_chain.chain:
# Skip empty Plain components
if isinstance(comp, Comp.Plain):
if comp.text and comp.text.strip():
has_result_chain_content = True
break
else:
# Non-Plain components (e.g., images, voice) are considered valid content
has_result_chain_content = True
break
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

检查 result_chain 是否包含有效内容的循环逻辑可以被 any() 和生成器表达式简化。这样代码会更简洁,也更符合 Python 的风格。

        has_result_chain_content = False
        if resp.result_chain and resp.result_chain.chain:
            has_result_chain_content = any(
                (isinstance(comp, Comp.Plain) and comp.text and comp.text.strip())
                or not isinstance(comp, Comp.Plain)
                for comp in resp.result_chain.chain
            )

Comment on lines +559 to +575
base_msg = "LLM returned empty assistant message with no tool calls."
model_id = getattr(self.run_context, "model_id", None)
provider_id = getattr(self.run_context, "provider_id", None)
run_id = getattr(self.run_context, "run_id", None)

ctx_parts = []
if model_id is not None:
ctx_parts.append(f"model_id={model_id}")
if provider_id is not None:
ctx_parts.append(f"provider_id={provider_id}")
if run_id is not None:
ctx_parts.append(f"run_id={run_id}")

if ctx_parts:
base_msg = f"{base_msg} Context: " + ", ".join(ctx_parts) + "."

raise LLMEmptyResponseError(base_msg)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

构造错误信息中上下文部分的代码可以通过使用字典和列表推导式来简化。这使得代码更简洁,并且在未来需要添加更多上下文属性时更易于扩展。

                base_msg = "LLM returned empty assistant message with no tool calls."
                ctx_info = {
                    "model_id": getattr(self.run_context, "model_id", None),
                    "provider_id": getattr(self.run_context, "provider_id", None),
                    "run_id": getattr(self.run_context, "run_id", None),
                }
                ctx_parts = [f"{k}={v}" for k, v in ctx_info.items() if v is not None]

                if ctx_parts:
                    base_msg = f"{base_msg} Context: {', '.join(ctx_parts)}."

                raise LLMEmptyResponseError(base_msg)

@Soulter Soulter changed the base branch from dev to master March 16, 2026 16:40
@dosubot dosubot bot added size:XXL This PR changes 1000+ lines, ignoring generated files. and removed size:M This PR changes 30-99 lines, ignoring generated files. labels Mar 16, 2026
@Soulter Soulter changed the base branch from master to dev March 16, 2026 16:40
@dosubot dosubot bot added size:M This PR changes 30-99 lines, ignoring generated files. and removed size:XXL This PR changes 1000+ lines, ignoring generated files. labels Mar 16, 2026
@a61995987 a61995987 changed the title fix:将上游空回复纳入回退重试并将空回复报错显示传递给用户 fix: 将上游空回复纳入回退重试并将空回复报错显示传递给用户 Mar 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:provider The bug / feature is about AI Provider, Models, LLM Agent, LLM Agent Runner. size:M This PR changes 30-99 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant